K:平衡二叉树(AVL)
相关介绍:
二叉查找树的查找效率与二叉树的形状有关,对于按给定序列建立的二叉排序树,若其左、右子树均匀分布,则查找过程类似于有序表的二分查找,时间复杂度变为O(log2n)。当若给定序列原来有序,则建立的二叉查找树就蜕化为单链表,其查找效率同顺序查找一样,时间复杂度为O(n)。因此,在构造二叉查找树的过程中,当出现左右子树分布不均匀时,若能对其进行调整,使其依然保持均匀,则就能有效的保证二叉查找树仍具有较高的查找效率。而平衡二叉树,正是这样的一棵树。
平衡二叉树,又称为AVL树,它或是一棵空树,或是一棵具有如下性质的二叉树:它的左子树和右子树均为平衡二叉树,且左子树和右子树深度之差的绝对值不超过1
在平衡二叉树上插入或删除节点后,可能使二叉树失去平衡。因此,需要对失去平衡的二叉树进行调整,以保持平衡二叉树的性质。以下,主要介绍如何动态地使一棵二叉查找树保持平衡,即对失去平衡的二叉查找树进行平衡化调整。这里引入了平衡因子的概念。
所谓的平衡因子,指的是二叉树中某个节点的左子树深度与右子树深度之差,平衡因子也称为平衡度。平衡二叉树也就是树中任意节点的平衡因子的绝对值小于等于1的二叉树。在AVL树中的节点的平衡因子有3种取值情况:1(左子树深度大于右子树深度),0(左子树深度等于右子树深度),-1(左子树深度小于右子树深度)
在以下给出的例子中,为了叙述方便,假设在AVL树上因插入新节点而失去平衡的最小子树的根节点为A,即A为距离插入节点最近的,平衡因子不是-1、0和1的节点。失去平衡后的操作可 依据失去平衡的原因(此处为便于记忆的诀窍的一个关键,如“LL型平衡旋转”,指的是,在A的左孩子节点(L)的左孩子节点(L)中插入一个新的节点所导致的不平衡问题) 归纳为下列4种情况分别进行介绍:
- LL型平衡旋转(单向右旋):由于在A的左孩子的左子树上插入新节点,使得A的平衡度由1增加至2,致使以A为根的子树失去平衡。此时,应该进行一次向右的顺时针旋转操作,“提升”B(即为A的左孩子)为新子树的根节点,A下降为B的右孩子,同时将B原来的右子树B-R调整为A的左子树。其过程如下图所示:
- RR型平衡旋转(单向左旋):由于在A的右孩子的右子树上插入新节点,使得A的平衡度由-1转变为-2,致使以A为根的子树失去平衡。此时,应该进行一次的向左的逆时针旋转操作,“提升”B(即A的右孩子)为新子树的根节点,A下降为B的左子树,同时将B原来的左子树B-L调整为A的右子树。其过程如下图所示:
- LR型平衡旋转(先左旋后右旋):由于在A的左孩子的右子树上插入新节点,使得A的平衡度由1变为2,致使以A为根的子树失去平衡。此时,应当进行两次选装操作(先逆时针,后顺时针)“提升”C(即A的左孩子的右孩子)为新子树的根节点;A下降为C的右孩子;B变为C的左孩子;C原来的左子树C-L调整为B现在的右子树;C原来的右子树C-R调整为A的左子树。其过程如下图所示:
或
- RL型平衡旋转(先右旋后左旋):由于在A的右孩子的左子树上插入新节点,使A的平衡度由-1变为-2,致使以A为根的子树失去平衡,此时,应进行两次旋转操作(先顺时针,后逆时针),“提升”C(即A的右孩子的左孩子)为新子树的根节点;A下降为C的左孩子;B变为C的右孩子;C原来的左子树C-L调整为A现在的右子树;C原来的右子树C-R调整为B的左子树。其过程如下图所示:
或
综上所述:在平衡二叉查找树T上插入一个新记录x的算法描述如下:
-
若AVL树为空树,则插入一个记录为x的新节点作为T的根节点,树的深度增加1。
-
若x的关键字值和AVL树T的根节点的关键字值相等,则不进行插入操作。
-
若X的关键字值小于AVL树的根节点的关键字值,则将x插入在该树的左子树上,并且当插入之后的左子树深度增加1时,分别就下列不同情况进行处理:
-
若AVL树的根节点的平衡因子为-1(右子树的深度大于左子树的深度),则将根节点的平衡因子调整为0,并且树的深度不变
-
若AVL树的根节点的平衡因子为0(左右子树的深度相等)。则将根节点的平衡因子调整为1,树的深度同时增加1
-
若AVL树的根节点的平衡因子为1(左子树的深度大于右子树的深度),则当该树的左子树的根节点的平衡因子为1时需要进行LL型平衡旋转;当该树的左子树的根节点的平衡因子为-1时需进行LR型平衡旋转
-
-
若x的关键字值大于AVL树的根节点的关键字值,则将x插入在该树的右子树上,并且当插入之后的右子树深度增加1时,分别就下列不同情况进行处理:
- 若AVL树的根节点的平衡因子为-1时(右子树的深度大于左子树的深度),则当该树的右子树的根节点的平衡因子为-1时需进行RR型平衡旋转;当该树的右子树的根节点的平衡因子为1时需进行RL型平衡旋转
2.若AVL树的根节点的平衡因子为0时(左右子树的深度相等),则将根节点的平衡因子调整为-1,树的深度同时增加1
- 若AVL树的根节点的平衡因子为1时(左子树的深度大于右子树的深度),则将根节点的平衡因子调整为0,并且树的深度保持不变
平衡二叉树的优缺点分析:
优点:使二叉树的结构更好(即不会出现“偏拐”严重的情况),从而提高了查找操作的速度。
缺点:使插入和删除操作复杂化,从而减低了插入和删除操作的速度。
总结:平衡二叉树适用于二叉查找树一经建立就很少进行插入和删除操作,而主要进行查找操作的应用场合上。由于其在查找过程中和给定值进行比较的关键字个数不超过树的深度。因此,在平衡二叉树上进行查找的时间复杂度为O(log2n)
相关操作示例代码:
对于平衡二叉树,其常见的操作有插入、删除、查找操作。其中,插入和删除某个节点时,需要对失去平衡的平衡二叉查找树进行相应的旋转操作,以使平衡二叉树保持结构上的稳定。
下面代码定义了平衡二叉查找树的节点:
相关代码:
/**
* 该类用于定义平衡二叉树的节点的相关数据
* @author 学徒
*
*/
class AVLTreeNode
{
//节点的关键字
Comparable key;
//节点的数据
Object data;
//节点的左子树指针
AVLTreeNode left;
//节点的右子树指针
AVLTreeNode right;
public AVLTreeNode(Comparable key,Object data)
{
this(key,data,null,null);
}
public AVLTreeNode(Comparable key,Object data,AVLTreeNode left,AVLTreeNode right)
{
this.data=data;
this.key=key;
this.left=left;
this.right=right;
}
}
对于查找操作,其先与根节点的关键字的值进行比较,根据比较所得的结果,选择返回其相关的数据或者是递归的在该节点的左右子树中进行查找
下面代码演示了查找的操作:
相关代码:
/**
* 用于平衡二叉树中的查找操作,并返回相应节点的相关数据,当未查找到时,返回null
* @param key 要进行查找的节点的关键字
* @return 得到要进行查找节点的相关数据
*/
public Object search(Comparable key)
{
if(key==null)
return null;
return getSearchResult(root,key);
}
/**
* 用于辅助平衡二叉树的查找操作,并返回相应的相关节点的数据,当未查找到时,返回null
* @param root 要进行查找的二叉查找树的根节点
* @param key 要进行查找的节点的关键字
* @return 得到要进行查找节点的相关数据
*/
private Object getSearchResult(AVLTreeNode root,Comparable key)
{
if(root==null)
return null;
int compare=key.compareTo(root.key);
if(compare==0)
return root.data;
else if(compare==1)
return getSearchResult(root.right,key);
else
return getSearchResult(root.left,key);
}
对于插入操作,先根据其关键字值的大小关系,在平衡二叉树中插入相应的节点。之后,遍历该新插入节点的路径中各个根节点的平衡因子的情况,根据其平衡因子的值,选择性的对其进行旋转操作,需要注意的是在该路径上最多只存在一次“失衡”的情况。为此,在对其路径上的节点进行过旋转操作之后,其平衡二叉树整体便是处于“平衡”的状态的
下面代码演示了插入操作:
相关代码:
/**
* 用于往二叉树中插入新的节点
* @param key 二叉树中新节点的关键字
* @param data 二叉树中新节点的数据
* @return 插入新节点的结果,如果成功插入一个新节点则返回true,否则返回false,当新增的节点存在于树中时,返回false
*/
public boolean insert(Comparable key,Object data)
{
if(key==null)
return false;
else if(root!=null)
{
return insertAVLTreeNode(root,null,key,data);
}
else
{
root=new AVLTreeNode(key,data);
return true;
}
}
/**
* 辅助往二叉树中插入新的节点
* @param node 要进行比较的二叉树的节点
* @param parent 要进行比较的二叉树的节点的父节点
* @param key 新增节点的关键字值
* @param data 新增节点的数据
* @return 新增的结果
*/
private boolean insertAVLTreeNode(AVLTreeNode node,AVLTreeNode parent,Comparable key,Object data)
{
//当当前比较的节点不为空的时候,进行比较操作
if(node!=null)
{
int compare=key.compareTo(node.key);
if(compare==0)
return false;
else if(compare>0)
return insertAVLTreeNode(node.right,node,key,data);
else
return insertAVLTreeNode(node.left,node,key,data);
}
//当前的节点为空的时候,进行从其父节点处插入节点的操作。同时,进行旋转判断和相应的操作
else
{
Comparable parentKey=parent.key;
AVLTreeNode newNode=new AVLTreeNode(key,data);
int comparable=parentKey.compareTo(key);
//插入新增节点
if(comparable>0)
parent.left=newNode;
else
parent.right=newNode;
//对插入完后的节点进行判断,查看其是否需要进行旋转操作,当需要时,对其进行进行旋转调整平衡二叉树相应的节点情况
rotate(root,null,newNode);
return true;
}
}
对于删除操作,其只需要在二叉树中找到要进行删除的节点的父节点。之后移动相应的指针即可对其进行删除操作,同时用其按照关键字有序的后继节点对其进行补充即可,之后从对到其进行删除节点的父节点的路径上的所有节点进行遍历,当存在因为删除节点而导致其失去平衡的情况,则对其进行相应的平衡旋转操作
下面代码演示了删除操作:
相关代码:
/**
* 用于进行删除操作
* @param key 要进行删除操作的节点的关键字值
* @return 返回所删除节点的数据值,当无相关的删除节点的时候,返回null
*/
public Object delete(Comparable key)
{
if(key==null)
return null;
else
return remove(root,key,null);
}
/**
* 用于辅助进行删除操作的方法
* @param node 当前比较节点
* @param parent 当前比较节点的双亲节点
* @param key 需要进行删除的节点的关键字值
* @return 返回所删除节点的关键字
*/
public Object remove(AVLTreeNode node,Comparable key,AVLTreeNode parent)
{
if (node != null)
{
int compare = key.compareTo(node.key);
// 从左子树中进行删除
if (compare < 0)
{
return remove(node.left, key, node);
}
// 从右子树中进行删除
else if (compare > 0)
{
return remove(node.right, key, node);
}
// 当前节点即为要进行删除的节点
else
{
// 当前要进行删除的节点的数据
Object result = node.data;
// 当前要进行删除的节点的左右子树均存在
if (node.left != null && node.right != null)
{
// 寻找要进行删除节点的替换节点
AVLTreeNode innext = node.right;
//要进行替换的节点的双亲节点
AVLTreeNode innextParent=node;
// 寻找右子树下的最左孩子节点
while (innext.left != null)
{
innextParent=innext;
innext = innext.left;
}
// 改变删除节点的相关数据
node.data = innext.data;
node.key = innext.key;
//递归的删除其进行替换的节点
remove(node.right,innext.key,node);
//对从当前被删除节点开始的到其相应的替换节点的父节点的路径上判断其是否“失衡”且进行相关的旋转操作
rotate(node,null,innextParent);
}
// 以下考虑的情况均当前删除节点缺少左子树或者右子树的情况
else
{
// 当前要进行删除的节点不为根节点的时候
if (parent != null)
{
// 当左子树不为空的时候
if (node.left != null && node.right == null)
{
// 当前节点为其左子树节点的时候
if (node == parent.left)
{
parent.left = node.left;
}
// 当前节点为其右子树节点的时候
else
{
parent.right = node.left;
}
if(node.left!=null)
{
//由于其删除节点缺少左子树或者右子树。为此,其只需判断当前删除节点的父节点的失衡情况并进行相应的调整即可
rotate(parent,null,node.left);
}
}
// 当右子树不为空的时候或为叶子节点的时候
else
{
// 当前节点为其左子树节点的时候
if (node == parent.left)
{
parent.left = node.right;
}
// 当前节点为其右子树节点的时候
else
{
parent.right = node.right;
}
if(node.right!=null)
{
//由于其删除节点缺少左子树或者右子树。为此,其只需判断当前删除节点的父节点的失衡情况并进行相应的调整即可
rotate(parent,null,node.right);
}
}
}
// 当前删除的节点为根节点的时候
else
{
if (node.left != null)
root = node.left;
else
root = node.right;
}
}
// 返回其进行删除的节点的值
return result;
}
}
return null;
}
完整示例代码如下:
相关代码:
package all_in_tree;
/**
* 该类用于演示平衡二叉树的相关操作
* @author 学徒
*
*/
public class AVLTree
{
//该平衡二叉树的根节点
private AVLTreeNode root;
/**
* 对该平衡二叉树进行后续遍历操作(后序遍历的结果是按序排序的),以便用于验证其测试的结果
*/
public static void inTravel(AVLTreeNode root)
{
if(root!=null)
{
inTravel(root.left);
System.out.print(root.key+"-"+root.data+"\t");
inTravel(root.right);
}
}
/**
* 用于获取该平衡二叉树
* @return 该平衡二叉树的根节点指针
*/
public AVLTreeNode getTree()
{
return this.root;
}
/**
* 用于得到一棵以root为根节点的树的深度
* @param root 需要获取的树的深度的根节点指针
* @return 以root为根节点的树的深度
*/
private int getDepth(AVLTreeNode root)
{
if(root==null)
return 0;
return Math.max(getDepth(root.left),getDepth(root.right))+1;
}
/**
* 用于判断以root为根节点的二叉树的平衡情况,true为平衡的,false表示并不平衡
* @param root 要判断其平衡因子的子树的根节点指针
* @return 以root为根节点的平衡二叉树的平衡情况
*/
private boolean isBalance(AVLTreeNode root)
{
return Math.abs(getDepth(root.left)-getDepth(root.right))<=1;
}
/**
* 用于获取以root为根节点的二叉树的平衡因子
* @param root为求取其平衡因子的子树的根节点的指针
* @return 平衡因子
*/
private int getBalance(AVLTreeNode root)
{
return getDepth(root.left)-getDepth(root.right);
}
/**
* 用于平衡二叉树中的查找操作,并返回相应节点的相关数据,当未查找到时,返回null
* @param key 要进行查找的节点的关键字
* @return 得到要进行查找节点的相关数据
*/
public Object search(Comparable key)
{
if(key==null)
return null;
return getSearchResult(root,key);
}
/**
* 用于辅助平衡二叉树的查找操作,并返回相应的相关节点的数据,当未查找到时,返回null
* @param root 要进行查找的二叉查找树的根节点
* @param key 要进行查找的节点的关键字
* @return 得到要进行查找节点的相关数据
*/
private Object getSearchResult(AVLTreeNode root,Comparable key)
{
if(root==null)
return null;
int compare=key.compareTo(root.key);
if(compare==0)
return root.data;
else if(compare==1)
return getSearchResult(root.right,key);
else
return getSearchResult(root.left,key);
}
/**
* 以所传入的节点的指针为根节点的二叉树,进行右旋的操作
* @param node 要进行右旋的二叉树的根节点
* @return 旋转后的二叉树的根节点的指针
*/
private AVLTreeNode rightRotate(AVLTreeNode node)
{
//用于记录旋转后的结果
AVLTreeNode result=node.left;
node.left=result.right;
result.right=node;
return result;
}
/**
* 以所传入的节点的指针为根节点的二叉树,进行左旋的操作
* @param node 要进行左旋的二叉树的根节点
* @return 旋转后的二叉树的根节点的指针
*/
private AVLTreeNode leftRotate(AVLTreeNode node)
{
//用于记录旋转后的结果
AVLTreeNode result=node.right;
node.right=result.left;
result.left=node;
return result;
}
/**
* 用于往二叉树中插入新的节点
* @param key 二叉树中新节点的关键字
* @param data 二叉树中新节点的数据
* @return 插入新节点的结果,如果成功插入一个新节点则返回true,否则返回false,当新增的节点存在于树中时,返回false
*/
public boolean insert(Comparable key,Object data)
{
if(key==null)
return false;
else if(root!=null)
{
return insertAVLTreeNode(root,null,key,data);
}
else
{
root=new AVLTreeNode(key,data);
return true;
}
}
/**
* 辅助往二叉树中插入新的节点
* @param node 要进行比较的二叉树的节点
* @param parent 要进行比较的二叉树的节点的父节点
* @param key 新增节点的关键字值
* @param data 新增节点的数据
* @return 新增的结果
*/
private boolean insertAVLTreeNode(AVLTreeNode node,AVLTreeNode parent,Comparable key,Object data)
{
//当当前比较的节点不为空的时候,进行比较操作
if(node!=null)
{
int compare=key.compareTo(node.key);
if(compare==0)
return false;
else if(compare>0)
return insertAVLTreeNode(node.right,node,key,data);
else
return insertAVLTreeNode(node.left,node,key,data);
}
//当前的节点为空的时候,进行从其父节点处插入节点的操作。同时,进行旋转判断和相应的操作
else
{
Comparable parentKey=parent.key;
AVLTreeNode newNode=new AVLTreeNode(key,data);
int comparable=parentKey.compareTo(key);
//插入新增节点
if(comparable>0)
parent.left=newNode;
else
parent.right=newNode;
//对插入完后的节点进行判断,查看其是否需要进行旋转操作,当需要时,对其进行进行旋转调整平衡二叉树相应的节点情况
rotate(root,null,newNode);
return true;
}
}
/**
* 用于辅助新插入节点是否旋转的判断,并作出相应的操作
* @param root 当前查找的节点的情况
* @param parent 当前查找节点的双亲节点
* @param node 进行旋转操作的路径的终止节点
*/
private void rotate(AVLTreeNode root,AVLTreeNode parent,AVLTreeNode node)
{
//由此得出当前查找节点的查找方向
int compare=root.key.compareTo(node.key);
//当找到该新增节点的时候,其路径上的各个节点的平衡因子均不“失衡”则直接进行返回,不进行操作
if(compare==0)
{
return;
}
//当平衡且当前节点的关键字值小于新增节点的关键字值的时候,往右边进行查找
else if(compare<0&&isBalance(root))
{
//当其路径上的根节点平衡的时候,往右边查找
rotate(root.right,root,node);
}
//当平衡且当前节点的关键字值大于新增节点的关键字值的时候,往左边进行查找
else if(compare>0&&isBalance(root))
{
//当其路径上的子树的根节点平衡的时候,往左边进行查找
rotate(root.left,root,node);
}
//当失衡的时候,对其进行旋转操作
else
{
//当其父节点为null的时候,不需要对其进行旋转操作,因为此时表示树中的节点数目不超过3个,所以一定不会有失衡的情况产生
if(parent!=null)
{
//当其失衡的时候,对其进行旋转操作
//用于判断是从其父节点的左边失衡的还是右边
boolean isLeft=parent.left.key.compareTo(root.key)==0?true:false;
//用于获取该失衡节点的平衡因子
int balance=getBalance(root);
//大于0的时候,要么进行LL型旋转,要么进行LR型旋转
if(balance>0)
{
//用于获取其孩子节点的平衡因子,根据其平衡因子的情况,进行相应的旋转操作
int childBalance=getBalance(root.left);
//对其相应的孩子节点为根节点的子树进行左旋
if(childBalance<0)
{
root.left=leftRotate(root.left);
}
//对以失衡的树的根节点为子树的树进行右旋操作
AVLTreeNode rotateNode=rightRotate(root);
if(isLeft)
parent.left=rotateNode;
else
parent.right=rotateNode;
}
//小于0的时候,要么进行RR型旋转,要么进行RL型旋转
else
{
//用于获取其孩子节点的平衡因子,根据其平衡因子的情况,进行相应的旋转操作
int childBalance=getBalance(root.right);
//对其相应的孩子节点为根的子树进行右旋
if(childBalance>0)
{
root.right=rightRotate(root.right);
}
//对以失衡的树的根节点为子树的树进行左旋操作
AVLTreeNode rotateNode=leftRotate(root);
if(isLeft)
parent.left=rotateNode;
else
parent.right=rotateNode;
}
}
}
}
/**
* 用于进行删除操作
* @param key 要进行删除操作的节点的关键字值
* @return 返回所删除节点的数据值,当无相关的删除节点的时候,返回null
*/
public Object delete(Comparable key)
{
if(key==null)
return null;
else
return remove(root,key,null);
}
/**
* 用于辅助进行删除操作的方法
* @param node 当前比较节点
* @param parent 当前比较节点的双亲节点
* @param key 需要进行删除的节点的关键字值
* @return 返回所删除节点的关键字
*/
public Object remove(AVLTreeNode node,Comparable key,AVLTreeNode parent)
{
if (node != null)
{
int compare = key.compareTo(node.key);
// 从左子树中进行删除
if (compare < 0)
{
return remove(node.left, key, node);
}
// 从右子树中进行删除
else if (compare > 0)
{
return remove(node.right, key, node);
}
// 当前节点即为要进行删除的节点
else
{
// 当前要进行删除的节点的数据
Object result = node.data;
// 当前要进行删除的节点的左右子树均存在
if (node.left != null && node.right != null)
{
// 寻找要进行删除节点的替换节点
AVLTreeNode innext = node.right;
//要进行替换的节点的双亲节点
AVLTreeNode innextParent=node;
// 寻找右子树下的最左孩子节点
while (innext.left != null)
{
innextParent=innext;
innext = innext.left;
}
// 改变删除节点的相关数据
node.data = innext.data;
node.key = innext.key;
//递归的删除其进行替换的节点
remove(node.right,innext.key,node);
//对从当前被删除节点开始的到其相应的替换节点的父节点的路径上判断其是否“失衡”且进行相关的旋转操作
rotate(node,null,innextParent);
}
// 以下考虑的情况均当前删除节点缺少左子树或者右子树的情况
else
{
// 当前要进行删除的节点不为根节点的时候
if (parent != null)
{
// 当左子树不为空的时候
if (node.left != null && node.right == null)
{
// 当前节点为其左子树节点的时候
if (node == parent.left)
{
parent.left = node.left;
}
// 当前节点为其右子树节点的时候
else
{
parent.right = node.left;
}
if(node.left!=null)
{
//由于其删除节点缺少左子树或者右子树。为此,其只需判断当前删除节点的父节点的失衡情况并进行相应的调整即可
rotate(parent,null,node.left);
}
}
// 当右子树不为空的时候或为叶子节点的时候
else
{
// 当前节点为其左子树节点的时候
if (node == parent.left)
{
parent.left = node.right;
}
// 当前节点为其右子树节点的时候
else
{
parent.right = node.right;
}
if(node.right!=null)
{
//由于其删除节点缺少左子树或者右子树。为此,其只需判断当前删除节点的父节点的失衡情况并进行相应的调整即可
rotate(parent,null,node.right);
}
}
}
// 当前删除的节点为根节点的时候
else
{
if (node.left != null)
root = node.left;
else
root = node.right;
}
}
// 返回其进行删除的节点的值
return result;
}
}
return null;
}
/**
* 测试用例
* @param args 初始化参数数组
*/
public static void main(String[] args)
{
AVLTree tree=new AVLTree();
tree.insert(5,"A");
tree.insert(2,"B");
tree.insert(6,"D");
tree.insert(1,"E");
tree.insert(3,"C");
tree.insert(4,"F");
AVLTree.inTravel(tree.getTree());
System.out.println();
System.out.println(tree.delete(1));
AVLTree.inTravel(tree.getTree());
System.out.println();
System.out.println(tree.search(5));
}
}
运行结果如下:
1-E 2-B 3-C 4-F 5-A 6-D
E
2-B 3-C 4-F 5-A 6-D
A
其测试用例所用图如下: